作者:近地小行星
链接:
https://juejin.cn/post/7241492331954012221
本文由作者授权发布。
前2篇文章探究了gradle是如何处理Task Graph和Task调度的,至此Task的前期工作就已经完成了。
下面就该执行Task了,如果观察过Task执行的话,会留意到console输出中Task后面有的带有执行结果的标识,如SKIPPED,UP-TO-DATE等。除了不带标识的和带有EXECUTED标识的表示是真正执行过Task的action的,其他的要么是从缓存中读取的结果,要么是不需要执行,这是gradle做的一个task执行的优化,下面也会针对gradle是通过什么判定出这些执行结果的。我们会看到gradle是如何将inputs/outputs的状态进行记录的,如何进行up-to-date的检测的,又是如何利用build cache来加速构建的完整流程。
Task的执行入口在LocalTaskNodeExecutor内,它从LocalTaskNode拿出Task,交给TaskExecuter来执行。在继续探究执行前,我们先看一下Task执行结果Outcome的类型。Task Outcome Task结果标识有5种,从名字上能大概看出它们的含义,在下面的执行过程中会看到这些结果产生的具体情况。SKIPPED
NO-SOURCE
UP-TO-DATE
FROM-CACHE
EXECUTED
TaskExecuter
TaskExecutor从名字上可以看出是用来执行Task的,它使用代理模式,将不同职责划分给了多个子类,我们看一下主要的几个。SkipOnlyIfTaskExecuter
tasks.register('customTask') {
onlyIf {
}
enabled = false
}
onlyIf和enabled都可以控制Task执行条件,如果其结果是false,那这个Task就不需要被执行,SkipOnlyIfTaskExecuter就是用来判断这个的。如果在控制台看到有Task执行结果后面带有SKIPPED标识,那么通常在这一步处理掉的。还有一种特殊情况,我们可以在Task的action中抛出StopExecutionException异常,这种情况输出结果后面不会带有SKIPPED标识,不是由SkipOnlyIfTaskExecuter处理,这种情况和上面有相同之处,在Task执行失败之后,依赖于它的Task依旧能够执行。SkipTaskWithNoActionsExecuter
如果Task没有action,那它就不需要执行,通常这些都是lifecycle tasks。• lifecycle tasks gradle的LifecycleBasePlugin有一些内置的lifecycle tasks,例如build,test,clean等。这些Task都没有action,它们代表了构建过程通用的一些逻辑,通常会让它们依赖 actionable tasks,例如java项目通过java plugin,让build依赖compileJava,kotlin项目会让其依赖compileKotlin。• actionable tasks 这些就是真正干活的Task了,它们都有action,有真正可以执行的逻辑在。这类Task的执行结果需要看其所依赖的Task的执行结果,如果依赖的Task都不是EXECUTED,那它的执行结果是UP-TO-DATE,否则为EXECUTED。ResolveTaskExecutionModeExecuter
这一步是通过分析Task的属性得出其执行模式,对后续步骤最主要的影响是其是否可以支持增量构建。INCREMENTAL
NO_OUTPUTS
RERUN_TASKS_ENABLED
UP_TO_DATE_WHEN_FALSE
UNTRACKED
- UNTRACKED 当Task被注解上了@UntrackedTask时。
- RERUN_TASKS_ENABLED 执行gradle命令时,后面加了--rerun-tasks时。
- NO_OUTPUTS Task outputs可以设置upToDateWhen来决定其是否复用之前的结果,如果Task既没有声明任何outputs属性,也没有设置upToDateWhen的话,为此执行模式。
- UP_TO_DATE_WHEN_FALSE 当Task的upToDateWhen返回false时。
- INCREMENTAL 其他情况时的执行模式,但Task是否能够真正增量执行还有很多因素影响。
这些类型主要是对下面3种属性的封装,这3种属性对Task的增量build有影响。从名称上比较容易理解,在后面的分析中会了解到它们如何影响构建过程。rebuildReason
taskHistoryMaintained
allowedToUseCachedResults
FinalizePropertiesTaskExecuter
将Property都finalize以确定下来,不再接受对属性的改动。Property在Task Graph篇章中有介绍过,这里就不再介绍了。以compileJava举例,就是classpath,destinationDirectory,sourceCompatibility,targetCompatibility等等一些参数。ExecuteActionsTaskExecuter
Task真正执行的地方,它会将Task转化为Unit Of Work去执行。UnitOfWork,从名字上理解,它是work的最小单元,gradle抽象的一个更细粒度的用来描述Work的接口,先来一张图以对其有个大致的理解。UnitOfWork的execute方法入参为ExecutionRequest,返回结果为WorkOutput。ExecutionRequest受InputVisitor和ExecutionBehavior的影响。WorkOutput也会被OutputVisitor和WorkResult进行分析。Identity是用来为UnitOfWork提供唯一标识用的。这些几乎都是接口,gradle制定了UnitOfWork的整体框架,剩下的实现部分并没有约束。
UnitOfWork的执行是由一系列的Step去执行的,Step和TaskExecuter一样使用了代理模式,它的实现更加复杂。Step
Context
Result
UnitOfWork
Step可以通过wrap的方式给Context添加一些参数给到下一个Step,比如workspace,上一次build的结果等。并且可以对上一个Step的执行结果Result进行一些操作,例如将上一步的执行结果保存到缓存中。Task的build cache、增量build等处理逻辑就是在这里处理的,下面来逐一分析Task的执行Step。Step的涉及到的流程很长,先用一张图来总览整体逻辑。IdentifyStep
IdentifyStep包了一层来提供自己的IdentityContext。IdentityContext主要负责提供标识,Task使用的是project的全路径加自身Task名字作为唯一标识。AssignWorkspaceStep
为Step的运行提供workspace,实际就是文件。UnitOfWork.getWorkspaceProvider方法会返回一个WorkspaceProvider,它用来提供work执行所需的workspace。withWorkspace表示会在某个工作目录下执行action,action会收到2个参数,workspace和history,history就是上一次构建的结果。一般是在withWorkspace中调用action的executeInWorkspace,Task的逻辑。public <T> T withWorkspace(String path, WorkspaceAction<T> action) {
return action.executeInWorkspace(null, context.getTaskExecutionMode().isTaskHistoryMaintained()
? executionHistoryStore
: null);
}
可以看到Task不会提供workspace,而history和Task执行模式有关,如果是taskHistoryMaintained=true的情况才会使用,否则为空,根据上面提到过的Task执行模式,也就是说NO_OUTPUTS,UNTRACKED这2种是不支持history。history具体的加载逻辑在后面LoadPreviousExecutionStateStep中。CleanupStaleOutputsStep
这是用来清理一些腐坏的build文件用的,主要是用于处理gradle版本更新场景的。也只针对能支持history的Task,因为这些Task才可能会产生输出,而这些输出的文件可能由于各种原因不正常了。这一步会删除outputs输出的文件中ownedByBuild且非gradle生成的文件,2个条件。2者其实都是通过文件路径来判断的,而判断ownedByBuild和generatedByGradle所通过的文件路径的集合是不一样的。通过调用BuildOutputCleanupRegistry.registerOutputs来将build目录添加进来
clean task将build目录添加进来了。java plugin将sourcSets的output文件目录加进来了。只要是位于BuildOutputCleanupRegistry中文件目录的文件,都属于是ownedByBuild的。RecordOutputStep会将输出的文件路径都保存下来,结果保存在 当前项目根目录/.gradle/buildOutputCleanup/outputFiles.bin 中。例如compileJava task会将build/classes/java/main、build/generated/sources/annotationProcessor/java/main等文件路径保存下来。jar task会将build/libs/xxx.jar路径保存下来。保存的是outputs属性指定的文件路径,其目录下的文件的路径是不会被保存的。所以compileJava task保存的是classes/java/main路径,这里面编译出来的classes文件路径是不处理的。虽然感觉它像是每次构建前会去删除不属于上次构建的文件。但实际如果没有历史构建记录的话,手动在build目录下新建一个文件确实会被删除掉,但是如果有历史构建记录outputFiles.bin,其判断方法是没法将新建的文件删掉的。它只会判断task outputs的文件路径,比如task a的outputs文件是build/output,那么只会check build/output,像build/output/other这样的是不会被check的。LoadPreviousExecutionStateStep
AssignWorkSpaceStep已经提供了ExecutionHistoryStore,这一步就是从history还原出上次的build执行状态。ExecutionHistoryStore接口也很简单,就3个方法,分别用来加载、保存和删除历史。这里需要重点看下这个PreviousExecutionState。这里的key是identity.getUniqueId(),对于task来说就是它的完整路径,如:lib:compileJava。这里的实现中会有一个keySerializer和一个valueSerializer,分别负责key和value的序列化和反序列化工作,这里的key为字符串所以不需要特别处理,valueSerializer会将缓存反序列化为PreviousExecutionState。这里序列化/反序列化具体实现使用的是之前在gradle脚本篇章中提到过的kryo三方库。保存的路径为 当前项目根目录/.gradle/8.0(gradle版本)/executionHistory/executionHistory.bin。ExecutionState的属性比较多,先看看脑图好有个整体印象。
- buildInvocationId 每次build都会生成一个uuid,此次build所有的task使用的都是这个id。
- 分为2种Class和Lambda,一般都是Class类型的。1. class identifier(class全路径名)
2. classLoaderHash 根据加载Task的classloader计算出来的hash值,gradle有很多classloader,用于加载gradle-api的,用于加载plugin的等等。Lambda还额外包括其实现的方法签名,实现类类型等信息。 - taskActionImplementations
与taskImplementation相似,记录的是Task内action的类型信息。 - 是一个key是属性名,value的类型为ValueSnapshot的map,ValueSnapshot有很多子类,是对各种原生类型,file,list,set,serializable等等的封装类。
- 从input file属性指定的文件提取的指纹信息,或者称为inputFilesFingerprints。key为属性名,value的类型为FileCollectionFingerprint的map。FileCollectionFingerprint,这是对FileSystemSnapshot,也就是从文件类型的快照提取的指纹信息,这也是一个map,key是文件absolutePath,value包含。
- fileType(RegularFile,Directory,Missing)
- contentHash - RegularFile是其内容的hash,Directory和Missing类型是常量,重要的就是这个hash值了。
- normalizedPath 根据normalization策略而来的path,具体在后续说明 rootHashes 基于子文件hash值计算出的hash。
strategyConfigurationHash 采用的normalization策略本身的hash值。
- outputFilesProducedByWork
outputs属性指定的输出文件的快照,返回值类型为FileSystemSnapshots。记录下整个outputs文件树结构的快照FileSystemSnapshot,本身包含absolutePath,name(文件名)属性,会遵守文件的顺序和文件的树形结构,分为3种类型。 - contentHash 基于子文件hash值计算出的hash 文件为RegularFileSnapshot,包含。
- length 缺失情况为MissingFileSnapshot。
- successful: Boolean 是否执行成功。
有3个ExecutionState,3者所包含的信息基本一致。PreviousExecutionState 上一次task执行后的状态。BeforeExecutionState 本次task执行前的状态。AfterExecutionState 本次task执行后的状态。ExecutionState记录了Task本身以及inputs/outputs的所有信息,这些信息有几个主要的作用。- 是用于和task上次执行的结果进行比较,如果属性全部没有改变过,那它符合up-to-date。
- 用于找出增量构建时发生改变的属性、文件等,具体在后面的ResolveChangesStep会详细说明。
MarkSnapshottingInputsStartedStep
RemoveUntrackedExecutionStateStep
这是执行善后工作的,它会先让后续Step执行完,执行的Result可能会带有一个 AfterExecutionState,用来记录本次执行的状态,和PreviousExecutionState对应。如果有PreviousExecutionState,那就会有AfterExecutionState。如果PreviousExecutionState没有,那AfterExecutionState也没有。而PreviousExecutionState是取决于是否支持history的,也就是说NO_OUTPUT, UNTRACKED这2种执行方式,在后续Step中有可能产生缓存,而在这一步会将其缓存清除掉。SkipEmptyWorkStep
gradle Task执行结果后面带有的NO_SOURCE标识,就是在这一步处理掉的。@SkipWhenEmpty的文件属性或者调用了skipWhenEmpty给属性强制设置不能为空,如果没有对应的inputs文件存在的话,会跳过它的执行,返回NO_SOURCE结果。不止如此,如果这个Task有上一次构建的历史文件存在,而这次没有inputs文件存在的话,会将上次的缓存清楚,此时执行结果是EXECUTED的。例如 Copy task,可以通过from和to来设置待复制的文件和目标路径。from最终是给Copy task添加一个source路径,而它给inputs设置了skipWhenEmpty 导致如果没有传入要拷贝的文件时,它实际不会执行。tasks.register('copy', Copy) {
}
Task :copy NO-SOURCE
Skipping task ':copy' as it has no source files and no previous output files.
CaptureStateBeforeExecutionStep
在LoadPreviousExecutionStateStep中我们对ExecutionState有了一定的了解,但是那里是从缓存中反序列化的数据,而在这里我们将会看到BeforeExecutionState是如何生成的。BeforeExecutionState记录的信息和PreviousExecutionState差不多,主要是记录当前的inputs/outputs情况,依旧是使用Visitor模式。Task和Action类型信息提取比较简单,这里不展开了,原生类型的属性也好处理,对于不同类型有相对应的ValueSnapshotter处理。重点是文件类型的InputFilesFingerprint的生成,和OverlappingOutputs的侦测。InputFilesFingerprints
前面有提到过fingerprints是从snapshot生成的,snapshot也有文件路径,文件hash相关的信息,那为什么还要有fingerprints呢?这还得回到记录inputs属性信息的目的上来,inputs信息的记录是为了对比2次构建,比较看是否有发生变化,已经发生了什么变化。那我们现在通过对文件进行snapshot操作,得到了目录的路径和hash,得到了目录内文件的路径和hash,记录着它们保存的顺序,看上去已经能够通过这些信息的对比来得到我们想要的东西了。
看上去相对路径更合理,但是否这样就满足所有需求了呢?比如有一个对jar包进行transform的action,只要jar包名称没变,内容没变,我就认为它是没有变化的,但是如果它生成的目录层级变化了,如果使用相对路径记录,就会认为它发生了变化。还有些情况,目录下面有空目录,这些路径是否需要记录,比如编译java代码的时候,这些空目录文件不会对结果产生任何影响,我们可以忽略掉,但如果记录了它们,那删除空目录会导致前后2次构建的inputs不同,而重新构建。- 文件的内容hash不变是否能等同于表示文件没有变?
那么反过来呢,有没有什么场景是我们虽然修改了文件,但我们这种改动对文件是没有影响的呢 比如properties文件,里面可以添加多个配置,如果加一行注释,该影响Task up-to-date的检测吗,如果将2个属性位置换一下又如何呢?再比如class path,我们在编译java代码的时候通常需要依赖,这些依赖都是通过jar包或者目录的方式添加到class path中的,这些jar包内添加了一些资源文件,又或者是某个private方法改了,需要我们的代码重新编译吗?所以针对这些问题,gradle需要对文件的snapshot进行fingerprint操作,这个过程也叫做normalization。Normalization
normalization有标准化,归一化的意思,影响normalization的主要有3个方面FileNormalizer、DirectorySensitivity和LineEndingSensitivity,下面我们来看看它们究竟都做了些什么。FileNormalizer
FileNormalizer主要影响normalizationPath和文件内容hash的生成,结合上面对文件路径的讨论,normalizationPath就是用来标准化文件路径的。- normalizationPath为绝对路径,这会对build cache的共享有影响,绝对路径不同会导致hash不同,缓存没法复用。默认是这个,所以自定义Task想要使用build cache时需要注意这点!
- 一般想要有缓存复用的属性尽量使用这个,这样就不会受项目目录的影响,也可以和其他机器共用缓存。
例如compileJava task的stableSources,也就是sourceSet定义的目录,默认是src/main。
位于根目录的文件,normalizedPath取文件名。
目录内的文件,normalizedPath取和根目录路径的相对路径。 - normalizedPath为文件名,文件名不变,层级改变也没关系。
Transformation,和@InputArtifact一起用的情况比较多,只要artifact的文件名、内容没变,outputs没变,层级变了不影响up-to-date的check。 - 例如Pmd plugin的ruleSetFiles使用的就是PathSensitivity.NONE,ruleSetFiles是xml文件,里面是对issue的一些自定义操作,比如排除掉对某些目录的检测等等,不关心xml的名称,只关心里面的内容是否发生了变化。
但有一点值得注意,使用PathSensitivity.NONE时,如果你改了脚本文件的文件路径,但是没有改动文件内容,虽然文件本身的hash没有改变,但是Action实现的hash可能因此改变,所以还是有变动的。
所以这个属性用在目录上更适合,其内部文件层级变动、名称改变不会产生影响,或者使用通配符的方式。
classpath情况比较复杂,需要单拎出来说,甚至要区分runtime和compile。用@CompileClasspath注解的属性,其normalization使用的即是CompileClasspath。指纹提取的逻辑在CompileClasspathFingerprinter中。compile classpath可能有目录和jar包,里面除了class文件外可能还有其他文件 它只关心class文件,文件的顺序也不关心,其中对class文件hash的工作是交由AbiExtractingClasspathResourceHasher处理的。AbiExtractingClasspathResourceHasher使用org.objectweb.asm库来从类字节码提取信息,对于private类会忽略,其他访问修饰符声明的类,将它们的public、protect、default声明的方法的方法名、返回值、入参类型、注解、异常抛出等等信息进行记录,还有对字段的相关信息的记录。ABI(application binary interface)
https://en.wikipedia.org/wiki/Application_binary_interface
ABI是二进制程序模块间的接口,通常是用machine code定义的数据结构、计算流程的访问,使用偏底层的,硬件依赖的格式。API是源码定义的,相对高级的,不依赖硬件且一般是可读的格式。实际上这里的ABI和API基本内容是一样的,这里说的API是指定义的外部可用的方法,字段等,通常是public的。因为拿到一个jar包,它里面public声明的类,方法等,其实我们都能够使用,就相当于是其暴露出来的API。只不过因为是从class字节码提取的信息,所以这里的ABI可以简单看作是API的字节码版本。下面这些case对于compile classpath没有影响,也就是说下面这些情况不会导致项目重新编译。- jar包内resources和manifest的变动,包括添加删除resources。
- class内private元素的改动,比如私有方法、私有fields、内部类等。
- 对方法体、静态初始化代码块,fields的初始化代码块等代码的改动(除了常量)。
- debug信息的改动,例如删除一行注释导致了debug信息行号变动。
- 对jar包内directories,包括directories内的entries的改动。
简单概括一下就是,声明了@CompileClasspath的属性,只会对其中的class文件进行hash,使用的是相对路径,对非private修饰的类,其中的非private的方法或者字段,其方法签名的改动,像是返回类型,参数增删,异常抛出等的修改会影响对改属性是否变动的判断。用@Classpath注解的属性,其normalization使用的即是RuntimeClasspath。RuntimeClasspath的hash策略并不像CompileClasspath会对class文件进行ABI信息提取,只是单纯的对文件内容进行hash。但RuntimeClasspath有一定的灵活性,可以通过脚本进行一些配置,比如忽略某些文件,使它们不对最终的hash造成影响,可以从properties、metaInf、resources3个方面进行自定义,相关API的使用可以参考configure_input_normalization。
https://docs.gradle.org/current/userguide/incremental_build.html#sec:configure_input_normalization
比如下面这个例子,忽略所有.properties文件中时间戳属性,它不会参与到hash的计算中去,如果timestamp变了,也不会影响到Task up-to-date的check。normalization {
runtimeClasspath {
properties {
ignoreProperty 'timestamp'
}
}
}
javaDoc就使用到了@Classpath注解。这里的CompileClasspath和RuntimeClasspath不是完全和java编译、执行过程的术语等同,更偏向于对这些类型的class path的通常的处理方式。这也能给缓存优化提供一些思路,java定义依赖有2种基础的方式api和implementation。其区别大家肯定都知道,api建立的依赖关系,它会导致transitive dependencies影响到项目的compile classpath。比如 projectA api 依赖了 libA, libA 又依赖了 libB,不管 libA 是用什么方式依赖的libB。projectA的compile classpath都会有 libB,那如果 libB 修改了影响ABI的代码,则会导致projectA rebuild,即使 projectA 内没有使用到任何 libB 的代码
这也是官方建议尽量使用implementation的原因。
DirectorySensitivity
使用注解@IgnoreEmptyDirectories,也有对应的api可以调用 compileJava task的source属性就标记上了这个注解,这个比较容易理解,源代码只要记录了相对路径就可以区分了,至于目录不需要我们是不关心的,而且可以排除掉空目录的影响。LineEndingSensitivity
使用注解@NormalizeLineEndings,也有对应的api可以调用。由于不同操作系统之间对换行的处理有可能是不一致的,这就导致如果使用了文件的原始内容,那么得到的hash值是没办法和其他操作系统的进行比较的。使用@NormalizeLineEndings注解,gradle计算hash时会将碰到的 \r,\r\n换行符都替换为\n,当然这些都是针对文本文件的,二进制文件的hash和snapshot的一样。gradle默认是不会使用替换换行符来计算hash的,所以另一种做法是让项目强制统一的换行符。总的来看,FileNormalizer有6种,DirectorySensitivity有2种,LineEndingSensitivity有2种。这几种是可以组合使用的,一共有24种排列组合方式。例如PathSensitivity.NONE这种情况本身就忽略了所有文件名称,所以本身也就对文件目录不敏感。OverlappingOutputs
gradle是期望不同Task的outputs目录都是不同的,没有重合,相互之间互不影响,但实际上可能会出现这种不同Task outputs目录重合的case,gradle将这种情况称为OverlappingOutput。OverlappingOutputs对增量构建、stale output的清除都会有影响,如果一个Task在另一个Task的outputs目录中也生成了文件,那么无法判断这个文件是否是stale的。找到OverlappingOutputs也比较简单,首先对Task当前的outputs文件进行snapshot,再和PreviousExecutionState的进行对比,previous和before的对比多出来的部分就是OverlappingOutputs,还能区分具体是哪个属性的。理解起来也比较简单,排除人为干扰的情况,正常在当前Task执行前,outputs的文件应该和PreviousExecutionState所记录的一致,如果有多出来的部分,那应该就是有后续Task的输出占用了同样的路径。ValidateStep
这一步主要验证@CacheableTask注解标注的Task的问题。如果其Task有标注@CacheableTask,那么其相应的@InputFile等注解需要额外标注上normalization相关注解。normalization在CaptureStateBeforeExecutionStep中有详细说明,这些会影响到缓存的有效性。Task默认都是@DisableCachingByDefault的。ResolveCachingStateStep
这一步是判断Task能否使用缓存,并生成BuildCacheKey。- 如果build_cache没有开启(org.gradle.caching为false)不能够使用缓存。
如果开启了,但是Task有验证问题也不行,因为只有error的问题会打断构建,warning的不会,所以需要先将所有error、warning等报错修复,才能使用caching。 - 如果Task被注解上了@DisableCachingByDefault的话那也不支持caching。
- 如果Task没有outputs文件,缓存就是针对输出的文件做的,没有输出自然不需要缓存。不止如此,如果outputs存在属性返回类型为FileTree,也是不支持caching的,返回File、FileCollection可以。
- cacheIf、doNotCacheIf中的判断条件也会有影响。
FileTree和FileCollection的区别是FileTree有层级,而FileCollection是展平的。详细可以参阅官方文档Working With Files。
https://docs.gradle.org/current/userguide/working_with_files.html#sec:file_trees
BuildCacheKey的生成逻辑基本上就是用BeforeExecutionState所记录的所有信息计算出一个hash值,具体有哪些信息在LoadPreviousExecutionStateStep中已经列出了。input属性: 属性name和具体的值都会参与key的计算,属性一般为原生类型,hash的计算比较容易。inputFilesFingerprint: 属性name和fingerprint的hash(也就是文件内容的hash)会参与key的计算。outputs属性: 只有属性name会参与key的计算。
MarkSnapshottingInputsFinishedStep
ResolveChangesStep
这一步是用来区分增量和非增量构建的,如果是增量构建,还需将此时的文件和上一次构建时的进行对比来生成InputChanges。- Task执行模式的属性rebuildReason。
- Task是否存在验证问题,有warning的task不支持。
5种执行模式中,只有INCREMENTAL没有rebuildReason,其他情况都有,也就是说其他的执行模式都是全量构建的。ResolveTaskExecutionModeExecuter小节中有列出不同执行模式rebuildReason。只有增量构建才可以使用构建缓存,其他几种类型的执行模式走到这意味着Task一定会被EXECUTED。NON_INCREMENTAL
INCREMENTAL
Task是根据自己是否有以InputChanges作为参数action来区别是否支持增量的
InputChanges有所有变动过的文件,以及它们变动的类型changeType,changeType可以区分是新增,删除还是修改,还有fileType可以区分目录和普通文件。abstract class IncrementalReverseTask extends DefaultTask {
@Incremental
@InputDirectory
abstract DirectoryProperty getInputDir()
@OutputDirectory
abstract DirectoryProperty getOutputDir()
@TaskAction
void execute(InputChanges inputChanges) {
inputChanges.getFileChanges(inputDir).each { change ->
def fileType = change.fileType == FileType.DIRECTORY
def targetFile = outputDir.file(change.normalizedPath).get().asFile
def changeType = change.changeType == ChangeType.REMOVED
}
}
}
NON_INCREMENTAL
INCREMENTAL
PRIMARY
这几种类型也是通过注解区分的,注解了@SkipWhenEmpty的是PRIMARY,注解了@Incremental是 INCREMENTAL,其他情况都是NON_INCREMENTAL,这些注解都是针对input files的。根据每个input files的属性注解的不同,其InputBehavior也可能不同。PRIMARY和INCREMENTAL都是支持增量的,它们的区别是在对inputs文件不存在时的处理上。PRIMARY是@SkipWhenEmpty的,所以会删除掉上次的构建记录。这里会将Task所有支持增量的input files属性进行搜集,后续InputChanges的生成需要用到。Detect Inputs Changes
确定是增量构建的情况下,gradle会去找出此次构建和上次构建input files间的区别,根据LoadPreviousExecutionStateStep加载的PreviousExecutionState上一次构建、CaptureStateBeforeExecutionStep记录的BeforeExecutionState本次构建、和收集到的incrementalInputProperties去找出inputs文件的改动。具体逻辑交由ExecutionStateChangeDetector.detectChanges处理。根据上面对LoadPreviousExecutionStateStep部分ExecutionState的描述,我们知道它包含了很多task的信息,基于这些信息来和本次的进行对比就可以得到2次构建input是否发生了改变,具体比较下面几个方面:- Task实现和Action实现是否有变动 比较本次和上次构建的Task及Actions的classIdentifier和classLoaderHash。
- input属性(非文件部分的属性)是否有变化 一方面需要对比是否有属性新增或减少,一方面需要比较具体的值是否发生了变化。
- input files属性是否有变化 input files属性是否有新增或减少。
非增量input files属性部分的文件是否有变动,这里通过比较fingerprint信息判断的。 - output files是否发生了变化 上面对input files的fingerprint做了详细的解释,但是output没提,其实output也有fingerprint,但是比inputs的简单太多了,只是用相对路径作为normalizationPath,因为它的变化主要是来自inputs,所以对于它指纹的提取没有必要那么复杂。
output files只用比较快照中的文件顺序、文件名、文件hash是否有区别就可以了。
如果上诉的情况有变化,就只能走非增量方式走full rebuild。如果没有变化,说明可以走增量构建,那么就需要对增量input file属性文件的变化信息进行收集。SkipToDateStep
上一步ResolveChangesStep已经让我们知道了Task是否增量构建,以及Input Changes。如果是支持增量构建的且input files没有变动,那么也就不需要执行了,这种情况的执行结果后面会打上UP-TO-DATE的标记,它会复用上一次的缓存结果。ResolveInputChangesStep
InputChanges核心部分已经在ResolveChangesStep处理完了,这里只是封装一下。StoreExecutionStateStep
这里是将执行结果状态AfterExecutionState进行保存,AfterExecutionState是由后续步骤生成的。只有执行成功,且outputs files有所变动才进行保存。outputs files的变动是通过对比AfterExecutionState和PreviousExecutionState得到的,比较过程和InputChanges中对outputs的对比一样。RecordOutputsStep
CaptureStateAfterExecutionStep会将Task的outputs快照记录下来,添加到AfterExecutionState中。这里就是将这些outputs文件路径保存下来,存在 当前项目根目录/.gradle/buildOutputCleanup/outputFiles.bin 里。也是CleanupStaleOutputsStep中用来判断文件是否由gradle生成的依据。BuildCacheStep
在ResolveCachingStateStep提到它是来判断是否可以使用缓存以及生成BuildCacheKey的。不能使用缓存的话就继续往下走,如果可以使用缓存的话,就从缓存中读取结果。Task执行Execution中,其实只有INCREMENTAL支持缓存读取,其他的几个都不支持,这个在ResolveCachingStateStep是没有处理的,因为虽然它不能读取缓存,但是它的执行结果可以被存到缓存中。缓存优先读取本地local的,如果本地没有就从读取远程缓存。本地缓存保存的目录为 ~/.gradle/caches/build-cache-1。远程缓存的读取会先请求服务端,实际就是以BuildCacheKey和服务器地址构造一个GET请求,如果结果返回正常,会将其保存到本地缓存中。缓存文件找到后需要对其进行解压,本质它是gzip压缩的文件,文件名就是key。以compileJava task为例,看看build cache文件缓存格式是什么样子的。METADATA
tree-destinationDirectory
tree-options.generatedSourceOutputDirectory
tree-options.headerOutputDirectory
tree-previousCompilationData
METADATA是记录一些元数据,里面有buildInvocationId、gradle版本、执行耗时、task名称等信息。然后每个output属性都会对应有以tree-属性名为名称的目录,里面保存着当时执行Task时生成的文件。gradle先是通过BuildCacheKey在本地缓存目录找到对应的gzip文件,然后unpack它,通过正则匹配到outputs属性的输出文件,进行复用。如果缓存读取失败,那么就会真正执行task,并在之后将其结果保存到缓存中,缓存会在local和server都进行保存。server端的保存是HttpBuildCacheService处理的,和读取类似,这里构建了一个PUT请求,将outputs文件pack为gzip文件上传。CaptureStateAfterExecutionStep
这一步会构造一个OriginMetadata,并将task的outputs指定的文件快照记录下来,作为AfterExecutionState的outputsProducedByWork。其他参数AfterExecutionState都是和BeforeExecutionState一样的。CreateOutputsStep
确保outputs属性指定的文件目录存在,对于目录类型会去创建,对于文件类型会创建其父目录。TimeoutStep
task可以设置超时时间,如果设置了超时时间,会启一个定时器到时interrupt Task的执行线程。CancelExecutionStep
Task可以被取消,被取消时也是通过interrupt Task的执行线程来实现的。RemovePreviousOutputsStep
这一步是针对预期增量构建,但因为某些原因导致没有进行增量构建的Task,删除其之前的outputs文件,例如某些input属性变动,导致需要重新构建。ExecuteStep
真正执行任务的step,也就是依次执行Task的actions。参考文档
Authoring Tasks
https://docs.gradle.org/current/userguide/more_about_tasks.html
Incremental build
https://docs.gradle.org/current/userguide/incremental_build.html
Developing Custom Gradle. Task Types
https://docs.gradle.org/current/userguide/custom_tasks.html
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
推荐阅读:
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!